from functools import reduce
from numpy import exp
from datetime import datetime
import minst


# 节点

def sigmod(val):
    return 1.0/(1 + exp(-val))

class Node(object):
    def __init__(self, nodeIndex, layerIndex):
        self.nodeIndex = nodeIndex
        self.layerIndex = layerIndex
        self.output = 0# 输入节点使用
        self.upstreamConns = []
        self.downstreamConns = []
        self.delta = 0
    
    def set_output(self, output):
        self.output = output
    
    def add_upstream_conn(self, conn):
        self.upstreamConns.append(conn)
    
    def add_downstream_conn(self, conn):
        self.downstreamConns.append(conn)
    
    def calc_hidden_node_delta(self):
        output = self.output
        downDelta = reduce(lambda val, conn: val+conn.downNode.delta*conn.weight, self.downstreamConns, 0)
        self.delta = output * (1-output) * downDelta
    
    def calc_output_node_delta(self, label):
        output = self.output
        self.delta = output * (1-output) * (label-output)
        return self.delta

    def calc_output(self):
        # 输入节点就直接使用output
        if self.layerIndex == 0: 
            return self.output
        res = reduce(lambda val, conn: val + conn.upNode.output*conn.weight, self.upstreamConns, 0)
        self.output = sigmod(res)
        return self.output
    
    def __str__(self):
        nodeStr = "(node)%u-%u: output %f delta %f" %(self.layerIndex, self.nodeIndex, self.output, self.delta)
        downstreamStr = "\n\t".join((str(conn) for conn in self.downstreamConns))
        upstreamStr = "\n\t".join((str(conn) for conn in self.upstreamConns))
        return '\n\t'.join((nodeStr, "downstream:"+downstreamStr, "upstream"+upstreamStr))

# b节点
class bNode(object):
    def __init__(self, nodeIndex, layerIndex):
        self.nodeIndex = nodeIndex
        self.layerIndex = layerIndex
        self.output = 1# 输入节点使用
        self.downstreamConns = []
    
    def calc_output(self):
        return self.output
    
    def add_downstream_conn(self, conn):
        self.downstreamConns.append(conn)

    def calc_hidden_node_delta(self):
        output = self.output
        downDelta = reduce(lambda val, conn: val+conn.downNode.delta*conn.weight, self.downstreamConns, 0)
        self.delta = output * (1-output) * downDelta


    def __str__(self):
        nodeStr = "(bNode)%u-%u: output %f delta %f" %(self.layerIndex, self.nodeIndex, self.output, self.delta)
        downstreamStr = "\n\t".join((str(conn) for conn in self.downstreamConns))
        return '\n\t'.join((nodeStr, "downstream:"+downstreamStr))

# 层
class Layer(object):
    def __init__(self, layerIndex, nodeCount):
        self.layerIndex = layerIndex
        self.nodeCount = nodeCount
        self.nodes = []
        for i in range(nodeCount):
            self.nodes.append(Node(i, layerIndex))
        # b常量节点
        self.nodes.append(bNode(nodeCount, layerIndex))
    
    def set_output(self, output):
        for i, val in enumerate(output):
            self.nodes[i].set_output(val)
    
    def calc_output(self):
        for node in self.nodes:
            node.calc_output()
    
    def update_weight(self, rate):
        for node in self.nodes:
            for conn in node.downstreamConns:
                conn.update_weight(rate)
    
    def calc_output_layer_delta(self, labels):
        for i, label in enumerate(labels):
            self.nodes[i].calc_output_node_delta(label)
    
    def calc_hidden_layer_delta(self):
        for node in self.nodes:
            node.calc_hidden_node_delta()

    

    
# 连接
class Connection(object):
    def __init__(self, upNode, downNode):
        self.upNode = upNode
        self.downNode = downNode
        self.upNode.add_downstream_conn(self)
        self.downNode.add_upstream_conn(self)
        self.weight = 0
        self.gradient = 0
    
    def calc_gradient(self):
        self.gradient = self.downNode.delta*self.upNode.output
        return self.gradient
    
    def update_weight(self, rate):
        self.calc_gradient()
        self.weight += self.gradient*rate
    
    def __str__(self):
        return "(%u-%u) -> (%u-%u) = %f" %(
            self.upNode.layerIndex, self.upNode.nodeIndex,
            self.downNode.layerIndex, self.downNode.nodeIndex,
            self.gradient
        )


# 网络
class Network(object):
    # layers包含每个层的节点个数
    def __init__(self, layers):
        self.layers = []
        self.connections = []
        for i, nodeNum in enumerate(layers):
            self.layers.append(Layer(i, nodeNum))
        # 初始每层的连接
        layerLen = len(self.layers)
        for i in range(layerLen-1):
            # 常量节点只有与后面节点的连接，没有与前面节点的连接，所以需要创建时需要注意(:-1是为了排除掉常量节点)
            self.connections = [Connection(upNode, downNode)
                for upNode in self.layers[i].nodes for downNode in self.layers[i+1].nodes[:-1]] 

    def train(self, samples, labels, rate, iteration):
        for j in range(iteration):
            print("train iteration %s" %(j))
            for i, sample in enumerate(samples):
                print("train sample %s itreation(%s)" %(i, j))
                self._train_one_sample(sample, labels[i], rate)
    
    def _train_one_sample(self, sample, labels, rate):
        self.predict(sample)
        self._update_delta(labels)
        self._update_weight(rate)
    
    def predict(self, sample):
        # 设置输入层
        self.layers[0].set_output(sample)
        for layer in self.layers[1:]:
            layer.calc_output()
        return map(lambda node: node.output, self.layers[-1].nodes[:-1])

    
    def _update_weight(self, rate):
        for layer in self.layers[:-1]:
            layer.update_weight(rate)

    
    def _update_delta(self, labels):
        self.layers[-1].calc_output_layer_delta(labels)
        for layer in self.layers[-2::-1]:
            layer.calc_hidden_layer_delta()
    
    def _calc_gradient(self):
        for conn in self.connections:
            conn.calc_gradient()


    def get_gradient(self, input, labels):
        self.predict(input)
        self._update_delta(labels)
        self._calc_gradient()
    
    def network_delta(self, outputs, labels):
        return 0.5*reduce(lambda val, vec: val + (vec[0]-vec[1])**2, zip(outputs, labels), 0)
    
    def gradient_check(self, input, labels):
        self.predict(input)
        self._update_delta(labels)
        self._calc_gradient()
        for conn in self.connections:
            actualGradient = conn.gradient

            # another way to compute gradient,then compare with actual
            weightDelta = 0.0004
            conn.weight += weightDelta
            delta1 = self.network_delta(self.predict(input), labels)
            conn.weight -= 2*weightDelta
            delta2 = self.network_delta(self.predict(input), labels)
            expectGradient = (delta2 - delta1) / (2*weightDelta)

            print("actual gradient \t%s\nexpect gradient \t%s" %(actualGradient, expectGradient))
            

def get_result(output):
    max = 0
    for i in range(1, len(output)):
        if output[max] < output[i]:
            max = i 
    return max

def evaluate(network, testSamples, testLabels):
    total = len(testSamples)
    error = 0
    for i in range(total):
        sample = testSamples[i]
        label = testLabels[i]
        output = network.predict(sample)
        val = get_result(output)
        if val != label:
            error += 1
    errorRate = float(error)/total
    print("total %d, error %d, erroRate %f" %(total, error, errorRate))
def main():
    mntImg = minst.MinstImage()
    trainMntImgPath = "D:/code/python/train_
    return errorRate 
data/train-images-idx3-ubyte/train-images.idx3-ubyte"
    mntImg.parse(trainMntImgPath)
    train_images = mntImg.get_images()

    mntLable = minst.MinstLabel()
    trainMntLablePath = r"D:\code\python\train_data\train-labels-idx1-ubyte\train-labels.idx1-ubyte"
    mntLable.parse(trainMntLablePath)
    train_labels = mntLable.get_labels()

    testMntImgPath = "D:/code/python/train_data/t10k-images-idx3-ubyte/t10k-images.idx3-ubyte"
    mntImg.parse(testMntImgPath)
    test_images = mntImg.get_images()


    testMntLablePath = r"D:\code\python\train_data\t10k-labels-idx1-ubyte\t10k-labels.idx1-ubyte"
    mntLable.parse(testMntLablePath)
    test_labels = mntLable.get_labels()

    network = Network([784, 300, 10])
    lastErrorRatio = 1.0
    index = 0
    while True:
        # every 10 train evalute
        network.train(train_images, train_labels, 0.3, 1)
        index += 1
        if index%10 == 0:
            errorRatio = evaluate(network, test_images, test_labels)
            print("%s after %s train errorRate %f" %(datetime.now(), index, errorRate))
            if errorRatio > lastErrorRatio:
                break 
            else:
                lastErrorRatio = errorRatio
        print("epoch: ", index)
        

if __name__ == "__main__":
    main()



